跳到主要内容

Go 的 HTTP 标准库-HTTP2.0 的使用

学习项目源码时,看到了使用关于 http2 的包,使用的是 h2c,所以这里记录一下这个包的使用

http2 协议的安全问题,实际上与http1协议一样,同样可选择http/https两种,一种是明文传输,另一种是加密传递。只不过在 google 设计 http2 协议的时候,偏向了加密传输,以至于 golang 1.6 中默认支持的 http2 协议包,现在却没有办法实现非加密 http2 通信了。

在实现中,http2 分为两个类型,普通的加密称称为 http2/h2,非加密的称为 http2/h2c。 在使用中,http2/h2 一般都有官方标准的实现,而 http2/h2c 则支持力度次之。

而由于加密 http2/h2 由于在TLS握手阶段,可能消耗约~10ms 时间,在有些场合是非常大的开销,所以还可能要用到 http2/h2c 实现方式。

比如在 grpc 中,服务器之间通信要求更高,就自带了一个 http2/h2c 的实现。

http2/h2 类型server实现

默认的 ListenAndServe 实际只支持 http1 协议,因为没有提供 TLS 支持,不会执行 golang 实现的 http2 协议逻辑。

func main() {
srv := &http.Server{
Addr: ":8080",
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println(r)
}),
}

srv.ListenAndServe()
}

如果要支持 http2 需要改成这样

srv.ListenAndServeTLS("local.cert", "local.key")

补充:这个证书和秘钥生成

# 生成私钥:
openssl genrsa -out key.pem 2048
# 生成证书:
openssl req -new -x509 -key key.pem -out cert.pem -days 3650

http2/h2c 类型server

标准的 golang 代码支持 HTTP2,但不直接支持 H2C。对 H2C 的支持只存在于 google 的 "golang.org/x/net/http2/h2c" 包中

如下创建一个 h2c 服务端,它支持 golang 本身支持的标准 HTTP/2和 HTTP/1.1

import (
"log"
"net"
"net/http"

"golang.org/x/net/http2/h2c"
"golang.org/x/net/http2"
)

func main() {
lsrv, err := net.Listen("tcp", ":8080")
if err != nil {
panic(err)
}

srv := http.Server{
Handler: h2c.NewHandler(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
log.Println(r)
rw.Write([]byte("hello world"))
}), &http2.Server{}),
}

if err := http2.ConfigureServer(&srv, &http2.Server{}); err != nil {
panic(err)
}

if err := srv.Serve(lsrv); err != nil {
panic(err)
}
}

这里使用 curl 工具发送 http2 请求

$ curl -v --http2 http://localhost:8080

检查控制台打印:

* Connected to localhost (::1) port 8080 (#0)
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.0
> Accept: */*
> Connection: Upgrade, HTTP2-Settings
> Upgrade: h2c
> HTTP2-Settings: AAMAAABkAARAAAAAAAIAAAAA
>
< HTTP/1.1 101 Switching Protocols
< Connection: Upgrade
< Upgrade: h2c
* Received 101
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Connection state changed (MAX_CONCURRENT_STREAMS == 250)!
< HTTP/2 200
< content-type: text/plain; charset=utf-8
< content-length: 11
< date: Tue, 25 Jan 2022 03:29:17 GMT
<
* Connection #0 to host localhost left intact
hello world%

可以发现是通过 HTTP/1.1连接,然后升级到 HTTP/2(H2C)

提示

注意看这里的 Connection: Upgrade 首部,浏览器不是通过这种方式升级使用 HTTP2 的,它是通过 HTTPS 的拓展字段进行协商升级的,具体看 HTTP2 那篇笔记

如果不需要支持 HTTP/1.1 可以这样写

func main() {
server := http2.Server{}
l, err := net.Listen("tcp", "0.0.0.0:8080")
log.Println(err, "while listening")
fmt.Printf("Listening [0.0.0.0:8080]...\n")

for {
conn, err := l.Accept()
log.Println(err, "during accept")

go func() {
server.ServeConn(conn, &http2.ServeConnOpts{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %v, http: %v", r.URL.Path, r.TLS == nil)
}),
})
}()
}
}

与 net/http 包使用方法不同,需要自己 listen,accept 并创建 goroutine 处理新连接请求。

这么实现的 server,为什么实现了 http2/h2c 协议呢? 因为我们跳过了 TLS 的握手协议创建加密连接一步,而是直接给 http2 请求处理函数一个明文的连接。

编写客户端连接

Golang 的标准库默认是不支持 h2c 的,所以标准包里面的客户端也不支持 h2c,所以必须重写 AllowHTTP

下面这个对接上面只支持 HTTP2 的服务端

import (
"crypto/tls"
"fmt"
"net"
"net/http"

"golang.org/x/net/http2"
)

func main() {
client := http.Client{
Transport: &http2.Transport{
// So http2.Transport doesn't complain the URL scheme isn't 'https'
AllowHTTP: true,
// Pretend we are dialing a TLS endpoint.
// Note, we ignore the passed tls.Config
DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) {
return net.Dial(network, addr)
},
},
}

resp, _ := client.Get("http://localhost:8080")
fmt.Printf("Client Proto: %d\n", resp.ProtoMajor)
}

输出打印:

Client Proto: 2

这里的两个参数:

  • AllowHTTP: 这个字段指定允许非TLS加密传输
  • DialTLS: 需要覆盖默认的创建连接函数。

References

HTTP/2 Cleartext (H2C) Client Example in Go golang/Go中HTTP2.0协议的应用